package de.akabahn.ebd.fst.impl;

import static de.akabahn.ebd.comm.Config.FSTCTRL_EOR_CHARS;
import static de.akabahn.ebd.comm.Config.FSTCTRL_PARAM_COMMAND;
import static de.akabahn.ebd.comm.Config.FSTCTRL_PARAM_TFZ;
import static de.akabahn.ebd.comm.Config.FSTCTRL_PORT;
import static de.akabahn.ebd.comm.Config.FSTCTRL_SERVER;
import static de.akabahn.ebd.comm.Config.FSTCTRL_WELCOME_MSG;
import static de.akabahn.ebd.comm.Config.FSTCTRL_PING_INTERVAL;
import static de.akabahn.ebd.comm.Config.FST_EOR_CHARS;
import static de.akabahn.ebd.comm.Config.FST_PORT;
import static de.akabahn.ebd.comm.Config.FST_SERVER;
import static de.akabahn.ebd.comm.Config.FST_WELCOME_MSG;
import static de.akabahn.ebd.comm.Util.assertInRange;
import static de.akabahn.ebd.comm.Util.assertNotNullAndEmpty;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Observer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import de.akabahn.ebd.comm.FstCtrlCommand;
import de.akabahn.ebd.comm.Util;
import de.akabahn.ebd.comm.telnet.TelnetConnectionClient;
import de.akabahn.ebd.fst.FahrSteuerungAdapter;
import de.akabahn.ebd.fst.FahrSteuerungAdapterFactory;
import de.akabahn.ebd.fst.FahrSteuerungException;
import de.akabahn.ebd.fst.Tfz;
import de.akabahn.ebd.fst.Tfz.Direction;
import de.akabahn.ebd.fst.Tfz.Mode;

/**
 * Class that manages the "Fahrsteuerung" application (connecting to the
 * "Fahrsteuerung" telnet server, sending commands to the server, receiving and
 * parsing the responses from the server).<br>
 * This class manages its lifecycle state, that means it has to be started and
 * stopped in order to use it.<br>
 * Instances of this class should be created by using the
 * <code>FahrSteuerungAdapterFactory</code> class.<br>
 * Currently supported commands are:<br>
 * <ul>
 * <li><code>INIT &lt;address&gt;</code></li>
 * <li><code>INF [&lt;adr&gt;] [...]</code></li>
 * <li><code>TERM &lt;address&gt;</code></li>
 * <li><code>STOP &lt;address&gt;</code></li>
 * <li><code>RICH &lt;address&gt; [&lt;direction&gt;]</code></li>
 * <li><code>UMK &lt;address&gt;</code></li>
 * <li><code>GO &lt;address&gt; &lt;speed&gt;</code></li>
 * <li><code>AUTO &lt;address&gt; &lt;track&gt;</code></li>
 * <li><code>SETF &lt;address&gt; &lt;function&gt; &lt;[0|1]&gt;</code></li>
 * <li><code>SETI &lt;address&gt; &lt;function&gt;</code></li>
 * <li><code>HOLD &lt;address&gt;</code></li>
 * <li><code>CONT &lt;address&gt;</code></li>
 * <li><code>BEF2 &lt;address&gt;</code></li>
 * <li><code>WAIT &lt;address&gt;</code></li>
 * <li><code>ENDW &lt;address&gt;</code></li>
 * <li><code>ZN &lt;address&gt; &lt;zn&gt;</code></li>
 * <li><code>ZN &lt;address&gt;</code></li>
 * <li><code>MT &lt;master&gt; +&lt;slave&gt;</code></li>
 * <li><code>MT &lt;master&gt; -&lt;slave&gt;</code></li>
 * <li><code>FERTIG &lt;address&gt;</code></li>
 * </ul>
 * 
 * @author Peter Eimann
 * @since 0.1
 * @version 1.0
 * @see FahrSteuerungAdapterFactory
 * @see #start()
 * @see #stop()
 */
public class FahrSteuerungAdapterImpl implements Runnable, FahrSteuerungAdapter {

	private static final String RESPONSE_ERROR = "Fehler";
	private static final String LOCATION_NONE = "(keiner)";
	private static final String TFZ_UMK = "Umk,";
	private static final String TFZ_PFEILR = "Pfeilr";
	private static final String TFZ_MULTITRAKTION = "<MULTITRAKTION>";
	private static final String TFZ_AUTOMATIC = "<AUTOM>";
	private static final String TFZ_HOLD = "<HOLD>";
	private static final String TFZ_WAIT = "<WAIT>";
	private static final String TFZ_BEFEHL2 = "<BEFEHL2>";
	private static final String TFZ_FAHRB = "<FAHRB>";
	private static final String TFZ_MT = "<MT>";

	/**
	 * Group identifier:<br>
	 * <ol>
	 * <li>Tfz address</li>
	 * <li>Speed</li>
	 * <li>Location</li>
	 * <li>Special function 0</li>
	 * <li>Special function 1</li>
	 * <li>Special function 2</li>
	 * <li>Special function 3</li>
	 * <li>Special function 4</li>
	 * </ol>
	 */
	private static final Pattern TFZ_INFO_PATTERN = Pattern
			.compile("^Tfz (\\d+):.+Fst (\\d+)\\,.+Ort ([\\w\\(\\)]+).+F([\\d|\\-])([\\d|\\-])([\\d|\\-])([\\d|\\-])([\\d|\\-]).+$");
	private static final Pattern TFZ_MT_PATTERN = Pattern
			.compile("^.+Multitraktion: gesteuert von (\\d+).+$");
	private static final Pattern TFZ_HNDR_PATTERN = Pattern
			.compile("^.+Auf Handregler (\\d+).+$");
	private static final Pattern TFZ_ZN_PATTERN = Pattern
			.compile(".+ Tfz \\d+ Zn (\\d+).*", Pattern.DOTALL);

	private boolean controlled;
	private boolean started;
	private TelnetConnectionClient telnetConnectionFst;
	private TelnetConnectionClient telnetConnectionFstCtrl;
	private Comparator<Tfz> tfzComparator;
	private Comparator<String> multitraktionComparator;
	private String lastResponse;
	private Exception pingLoopException;
	protected ExecutorService threadPool;
	protected CountDownLatch pingLatch;

	/**
	 * Initialize the instance by creating a new
	 * <code>TelnetConnectionClient</code> instance and adding the observer
	 * objects to it.<br>
	 * The connection is made to the Fahrsteuerung server defined by the
	 * <code>FST_SERVER</code> (192.168.2.5) and <code>FST_PORT</code> (25555)
	 * constants and to the Fahrsteuerung-Control server defined by the
	 * <code>FSTCTRL_SERVER</code> (192.168.2.5) and <code>FSTCTRL_PORT</code>
	 * (9125) constants.
	 * 
	 * @param controlled
	 *            Flag to indicate whether the adapter should be controlled by
	 *            the <code>FahrSteuerungControl</code> application or not
	 * @param connectionObservers
	 *            The observer classes that want to get notified when the
	 *            connection status to the telnet server changes
	 * @see #FahrSteuerungAdapterImpl(String, int, String, int, boolean,
	 *      Observer...)
	 */
	public FahrSteuerungAdapterImpl(boolean controlled, Observer... connectionObservers) {
		this(FST_SERVER, FST_PORT, FSTCTRL_SERVER, FSTCTRL_PORT, controlled, connectionObservers);
	}

	/**
	 * Initialize the instance by creating a new
	 * <code>TelnetConnectionClient</code> instance and adding the observer
	 * objects to it.
	 * 
	 * @param fstServer
	 *            The server to connect to for the Fahrsteuerung
	 * @param fstPort
	 *            The port to connect to for the Fahrsteuerung
	 * @param fstCtrlServer
	 *            The server to connect to for the Fahrsteuerung-Control (can be
	 *            <code>null</code> if the adapter should not be controlled)
	 * @param fstCtrlPort
	 *            The port to connect to for the Fahrsteuerung-Control (can be
	 *            <code>null</code> if the adapter should not be controlled)
	 * @param controlled
	 *            Flag to indicate whether the adapter should be controlled by
	 *            the <code>FahrSteuerungControl</code> application or not
	 * @param connectionObservers
	 *            The observer classes that want to get notified when the
	 *            connection status to the telnet server changes
	 */
	public FahrSteuerungAdapterImpl(String fstServer, int fstPort,
			String fstCtrlServer, int fstCtrlPort, boolean controlled,
			Observer... connectionObservers) {
		this.controlled = controlled;

		telnetConnectionFst = new TelnetConnectionClient(fstServer, fstPort, FST_EOR_CHARS, FST_WELCOME_MSG);

		if (this.controlled) {
			telnetConnectionFstCtrl = new TelnetConnectionClient(fstCtrlServer,
					fstCtrlPort, FSTCTRL_EOR_CHARS, FSTCTRL_WELCOME_MSG);
		}

		if (connectionObservers != null) {
			for (Observer connectionObserver : connectionObservers) {
				telnetConnectionFst.addObserver(connectionObserver);
				if (this.controlled) {
					telnetConnectionFstCtrl.addObserver(connectionObserver);
				}
			}
		}

		tfzComparator = new TfzComparator();
		multitraktionComparator = new MultitraktionComparator();
	}

	/**
	 * Establish the connection to the remote telnet server and set the
	 * lifecycle status of this instance to <code>started</code>. If it's
	 * already started, nothing happens.
	 * 
	 * @throws FahrSteuerungException
	 */
	@Override
	public synchronized void start() throws FahrSteuerungException {
		if (!started) {
			try {
				telnetConnectionFst.connect();
				startPingLoop();
			} catch (RuntimeException re) {
				throw new FahrSteuerungException("Start failed", re);
			}
			started = true;
		}
	}

	/**
	 * Disconnect from the remote telnet server and set the lifecycle status of
	 * this instance to <code>stopped</code>. If it's already stopped, nothing
	 * happens.
	 * 
	 * @throws FahrSteuerungException
	 */
	@Override
	public synchronized void stop() throws FahrSteuerungException {
		if (started) {
			try {
				stopPingLoop();
				telnetConnectionFst.disconnect();
			} catch (RuntimeException re) {
				throw new FahrSteuerungException("Stop failed", re);
			}
			started = false;
			lastResponse = null;
		}
	}

	/**
	 * Return if the current lifecycle status is <code>started</code> or not
	 * 
	 * @return <code>true</code> if the lifecylce status is <code>started</code>
	 */
	@Override
	public boolean isStarted() {
		return started;
	}

	/**
	 * Register the "Triebfahrzeug" with the specified address at the
	 * "Fahrsteuerung" by sending the command <code>INIT &lt;address&gt;</code>
	 * to the server.
	 * 
	 * @param address
	 *            The address of the "Triebfahrzeug"
	 * @return <code>true</code> if the registration was successful
	 * @throws IllegalArgumentException
	 *             thrown if the input parameters are invalid
	 * @throws FahrSteuerungException
	 * @see #parseResponse(String)
	 * @see #getLastResponse()
	 */
	@Override
	public boolean init(String address) throws FahrSteuerungException {
		String command = null;
		String response = null;

		assertIsStarted();
		assertNotNullAndEmpty(address, "address");

		command = "INIT " + address;
		response = sendAndReceive(command, telnetConnectionFst);

		try {
			return parseResponse(response);
		} catch (RuntimeException re) {
			throw new FahrSteuerungException("Parsing INIT response failed", re);
		}
	}

	/**
	 * Return a list containing information about all the "Triebfahrzeuge" that
	 * are currently registered by sending the command <code>INF</code> to the
	 * server.
	 * 
	 * @return The list of registered "Triebfahrzeuge" (will be
	 *         <code>empty</code> and not <code>null</code> if none are
	 *         registered ), ordered by their address and mode
	 * @throws FahrSteuerungException
	 * @see #inf(String...)
	 * @see #getLastResponse()
	 * @see Tfz
	 * @see TfzComparator
	 */
	@Override
	public List<Tfz> inf() throws FahrSteuerungException {
		return inf("");
	}

	/**
	 * Return a list containing information about the "Triebfahrzeuge" with the
	 * given address by sending the command <code>INF [&lt;adr&gt;] [...]</code>
	 * . Multiple addresses may be specified.<br>
	 * The query for "Triebfahrzeuge" at a given track is not supported because
	 * the application then only returns the tfz addresses without any other
	 * information. If <code>tfzList</code> is empty, the list will contain all
	 * registered "Triebfahrzeuge".
	 * 
	 * @param tfzList
	 *            The address (only "Triebfahrzeuge" with this address are
	 *            returned), if all registered "Triebfahrzeuge" should be
	 *            returned, an <code>empty string</code> has to be used
	 * @return The list of registered "Triebfahrzeuge" with the specified
	 *         address (will be <code>empty</code> and not <code>null</code> if
	 *         none are registered), ordered by their address and mode
	 * @throws IllegalArgumentException
	 *             thrown if the input parameters are invalid
	 * @throws FahrSteuerungException
	 * @see #inf()
	 * @see #parseInfResponse(String)
	 * @see #getLastResponse()
	 * @see Tfz
	 * @see TfzComparator
	 */
	@Override
	public List<Tfz> inf(String... tfzList) throws FahrSteuerungException {
		String response = null;

		assertIsStarted();
		if (tfzList == null) {
			throw new IllegalArgumentException("tfzList must not be null");
		}

		StringBuilder command = new StringBuilder("INF ");
		for (String tfz : tfzList) {
			if (!tfz.matches("^$|\\d+")) {
				throw new IllegalArgumentException("Only tfz addresses are allowed");
			}
			command.append(tfz).append(" ");
		}

		response = sendAndReceive(command.toString().trim(),
				telnetConnectionFst);

		try {
			return parseInfResponse(response);
		} catch (RuntimeException re) {
			throw new FahrSteuerungException("Parsing INF response failed", re);
		}
	}

	/**
	 * Deregister the "Triebfahrzeug" with the specified address at the
	 * "Fahrsteuerung" by sending the command <code>TERM &lt;address&gt;</code>
	 * to the server.
	 * 
	 * @param address
	 *            The address of the Triebfahrzeug
	 * @return <code>true</code> if the deregistration was successful
	 * @throws IllegalArgumentException
	 *             thrown if the input parameters are invalid
	 * @throws FahrSteuerungException
	 * @see #parseResponse(String)
	 * @see #getLastResponse()
	 */
	@Override
	public boolean term(String address) throws FahrSteuerungException {
		String command = null;
		String response = null;

		assertIsStarted();
		assertNotNullAndEmpty(address, "address");

		command = "TERM " + address;
		response = sendAndReceive(command, telnetConnectionFst);
		sendToFahrSteuerungControl(FstCtrlCommand.STOP, address);

		try {
			return parseResponse(response);
		} catch (RuntimeException re) {
			throw new FahrSteuerungException("Parsing TERM response failed", re);
		}
	}

	/**
	 * Stop the "Triebfahrzeug" with the specified address and switch it to
	 * manual operation mode by sending the command
	 * <code>STOP &lt;address&gt;</code> to the server.
	 * 
	 * @param address
	 *            The address of the Triebfahrzeug
	 * @return <code>true</code> if the stop was successful
	 * @throws IllegalArgumentException
	 *             thrown if the input parameters are invalid
	 * @throws FahrSteuerungException
	 * @see #parseResponse(String)
	 * @see #getLastResponse()
	 */
	@Override
	public boolean stop(String address) throws FahrSteuerungException {
		String command = null;
		String response = null;

		assertIsStarted();
		assertNotNullAndEmpty(address, "address");

		command = "STOP " + address;
		response = sendAndReceive(command, telnetConnectionFst);
		sendToFahrSteuerungControl(FstCtrlCommand.STOP, address);

		try {
			return parseResponse(response);
		} catch (RuntimeException re) {
			throw new FahrSteuerungException("Parsing STOP response failed", re);
		}
	}

	/**
	 * Convenience method if the direction of the "Triebfahrzeug" should just be
	 * changed instead of setting it to a concrete value.
	 * 
	 * @param address
	 *            The address of the Triebfahrzeug
	 * @return <code>true</code> if the direction change was successful
	 * @throws FahrSteuerungException
	 * @see #rich(String, Tfz.Direction)
	 * @see #getLastResponse()
	 */
	@Override
	public boolean rich(String address) throws FahrSteuerungException {
		return rich(address, null);
	}

	/**
	 * Set or change the direction of the "Triebfahrzeug" with the specified
	 * address by sending the command
	 * <code>RICH &lt;address&gt; [&lt;direction&gt;]</code> to the server. If
	 * the <code>direction</code> is not specified, the current direction of the
	 * "Triebfahrzeug" is changed, otherwise the direction is set to the
	 * specified value.
	 * 
	 * @param address
	 *            The address of the Triebfahrzeug
	 * @param direction
	 *            The new direction, <code>REVERSE</code> means reverse
	 *            direction, <code>ARROW</code> means arrow direction. If set to
	 *            <code>null</code>, the current direction is changed.
	 * @return <code>true</code> if the direction change was successful
	 * @throws IllegalArgumentException
	 *             thrown if the input parameters are invalid
	 * @throws FahrSteuerungException
	 * @see #parseResponse(String)
	 * @see #rich(String)
	 * @see #getLastResponse()
	 */
	@Override
	public boolean rich(String address, Direction direction) throws FahrSteuerungException {
		String response = null;

		assertIsStarted();
		assertNotNullAndEmpty(address, "address");

		StringBuilder command = new StringBuilder("RICH ").append(address);
		if (direction != null) {
			command.append(" ").append(direction.getValue());
		}

		response = sendAndReceive(command.toString(), telnetConnectionFst);
		sendToFahrSteuerungControl(FstCtrlCommand.STOP, address);

		try {
			return parseResponse(response);
		} catch (RuntimeException re) {
			throw new FahrSteuerungException("Parsing RICH response failed", re);
		}
	}

	/**
	 * Reverse the direction of the "Triebfahrzeug" with the specified address.
	 * 
	 * @param address
	 *            The address of the Triebfahrzeug
	 * @return <code>true</code> if the reverse was successful
	 * @throws IllegalArgumentException
	 *             thrown if the input parameters are invalid
	 * @throws FahrSteuerungException
	 */
	@Override
	public boolean umk(String address) throws FahrSteuerungException {
		String command = null;
		String response = null;

		assertIsStarted();
		assertNotNullAndEmpty(address, "address");

		command = "UMK " + address;
		response = sendAndReceive(command, telnetConnectionFst);

		try {
			return parseResponse(response);
		} catch (RuntimeException re) {
			throw new FahrSteuerungException("Parsing UMK response failed", re);
		}
	}

	/**
	 * Set the speed of the "Triebfahrzeug" by sending the command
	 * <code>GO &lt;address&gt; &lt;speed&gt;</code> to the server. Speed can be
	 * in the range between 0 and 100, where 0 means stop and 100 maximum speed.
	 * 
	 * @param address
	 *            The address of the Triebfahrzeug
	 * @param speed
	 *            The speed of the Triebfahrzeug
	 * @return <code>true</code> if the speed change was successful
	 * @throws IllegalArgumentException
	 *             thrown if the input parameters are invalid
	 * @throws FahrSteuerungException
	 * @see #parseResponse(String)
	 * @see #getLastResponse()
	 */
	@Override
	public boolean go(String address, int speed) throws FahrSteuerungException {
		String command = null;
		String response = null;

		assertIsStarted();
		assertNotNullAndEmpty(address, "address");
		assertInRange(speed, "speed", 0, 100);

		if (speed > 0) {
			sendToFahrSteuerungControl(FstCtrlCommand.GO, address);
		}

		command = "GO " + address + " " + Integer.toString(speed);
		response = sendAndReceive(command, telnetConnectionFst);

		if (speed == 0) {
			sendToFahrSteuerungControl(FstCtrlCommand.STOP, address);
		}

		try {
			return parseResponse(response);
		} catch (RuntimeException re) {
			throw new FahrSteuerungException("Parsing GO response failed", re);
		}
	}

	/**
	 * Set the "Triebfahrzeug" in automatic operation by sending the command
	 * <code>AUTO &lt;address&gt; &lt;track&gt;</code> to the server. Track
	 * specifies the current location of the "Triebfahrzeug".
	 * 
	 * @param address
	 *            The address of the Triebfahrzeug
	 * @param track
	 *            The location of the Triebfahrzeug
	 * @return <code>true</code> if setting the automatic mode was successful
	 * @throws IllegalArgumentException
	 *             thrown if the input parameters are invalid
	 * @throws FahrSteuerungException
	 * @see #parseResponse(String)
	 * @see #getLastResponse()
	 */
	@Override
	public boolean auto(String address, String track) throws FahrSteuerungException {
		String command = null;
		String response = null;

		assertIsStarted();
		assertNotNullAndEmpty(address, "address");
		assertNotNullAndEmpty(track, "track");

		command = "AUTO " + address + " " + track;
		response = sendAndReceive(command, telnetConnectionFst);

		try {
			return parseResponse(response);
		} catch (RuntimeException re) {
			throw new FahrSteuerungException("Parsing AUTO response failed", re);
		}
	}

	/**
	 * Activate or deactivate one of five possible special functions of a
	 * "Triebfahrzeug" by sending the command
	 * <code>SETF &lt;address&gt; &lt;function&gt; &lt;[0|1]&gt;</code> to the
	 * server.
	 * 
	 * @param address
	 *            The address of the Triebfahrzeug
	 * @param function
	 *            The function to activate or deactivate
	 * @param activate
	 * 			  <code>null</code> if the function should be toggled,
	 *            <code>true</code> if the function should be activated, 
	 *            <code>false</code> it it should be deactivated
	 * @return <code>true</code> if setting the function was successful
	 * @throws IllegalArgumentException
	 *             thrown if the input parameters are invalid
	 * @throws FahrSteuerungException
	 * @see #parseResponse(String)
	 * @see #getLastResponse()
	 */
	@Override
	public boolean setf(String address, int function, Boolean activate) throws FahrSteuerungException {
		String command = null;
		String response = null;

		assertIsStarted();
		assertInRange(function, "function", 0, 4);

		command = "SETF " + address + " " + function;
		if (activate != null) {
			command = command + " " + ((activate) ? "1" : "0");
		}
		response = sendAndReceive(command, telnetConnectionFst);

		try {
			return parseResponse(response);
		} catch (RuntimeException re) {
			throw new FahrSteuerungException("Parsing SETF response failed", re);
		}
	}

	/**
	 * Activate five possible special functions of a "Triebfahrzeug" for a
	 * limited time by sending the command
	 * <code>SETI &lt;address&gt; &lt;function&gt;</code> to the server.
	 * 
	 * @param address
	 *            The address of the Triebfahrzeug
	 * @param function
	 *            The function to activate
	 * @return <code>true</code> if setting the function was successful
	 * @throws IllegalArgumentException
	 *             thrown if the input parameters are invalid
	 * @throws FahrSteuerungException
	 * @see #parseResponse(String)
	 * @see #getLastResponse()
	 */
	@Override
	public boolean seti(String address, int function) throws FahrSteuerungException {
		String command = null;
		String response = null;

		assertIsStarted();
		assertNotNullAndEmpty(address, "address");
		assertInRange(function, "function", 0, 4);

		command = "SETI " + address + " " + function;
		response = sendAndReceive(command, telnetConnectionFst);

		try {
			return parseResponse(response);
		} catch (RuntimeException re) {
			throw new FahrSteuerungException("Parsing SETI response failed", re);
		}
	}

	/**
	 * Hold the "Triebfahrzeug" with the specified address by sending the
	 * command <code>HOLD &lt;address&gt;</code> to the server.
	 * 
	 * @param address
	 *            The address of the Triebfahrzeug
	 * @return <code>true</code> if the hold was successful
	 * @throws IllegalArgumentException
	 *             thrown if the input parameters are invalid
	 * @throws FahrSteuerungException
	 * @see #parseResponse(String)
	 * @see #getLastResponse()
	 */
	@Override
	public boolean hold(String address) throws FahrSteuerungException {
		String command = null;
		String response = null;

		assertIsStarted();
		assertNotNullAndEmpty(address, "address");

		command = "HOLD " + address;
		response = sendAndReceive(command, telnetConnectionFst);

		try {
			return parseResponse(response);
		} catch (RuntimeException re) {
			throw new FahrSteuerungException("Parsing HOLD response failed", re);
		}
	}

	/**
	 * End the hold mode for "Triebfahrzeug" with the specified address by
	 * sending the command <code>CONT &lt;address&gt;</code> to the server.
	 * 
	 * @param address
	 *            The address of the Triebfahrzeug
	 * @return <code>true</code> if the cont was successful
	 * @throws IllegalArgumentException
	 *             thrown if the input parameters are invalid
	 * @throws FahrSteuerungException
	 * @see #parseResponse(String)
	 * @see #getLastResponse()
	 */
	@Override
	public boolean cont(String address) throws FahrSteuerungException {
		String command = null;
		String response = null;

		assertIsStarted();
		assertNotNullAndEmpty(address, "address");

		command = "CONT " + address;
		response = sendAndReceive(command, telnetConnectionFst);

		try {
			return parseResponse(response);
		} catch (RuntimeException re) {
			throw new FahrSteuerungException("Parsing CONT response failed", re);
		}
	}

	/**
	 * Expect the next signal to be clear for the "Triebfahrzeug" with the
	 * specified address by sending the command
	 * <code>BEF2 &lt;address&gt;</code> to the server.
	 * 
	 * @param address
	 *            The address of the Triebfahrzeug
	 * @return <code>true</code> if the bef2 was successful
	 * @throws IllegalArgumentException
	 *             thrown if the input parameters are invalid
	 * @throws FahrSteuerungException
	 * @see #parseResponse(String)
	 * @see #getLastResponse()
	 */
	@Override
	public boolean bef2(String address) throws FahrSteuerungException {
		String command = null;
		String response = null;

		assertIsStarted();
		assertNotNullAndEmpty(address, "address");

		command = "BEF2 " + address;
		response = sendAndReceive(command, telnetConnectionFst);

		try {
			return parseResponse(response);
		} catch (RuntimeException re) {
			throw new FahrSteuerungException("Parsing BEF2 response failed", re);
		}
	}

	/**
	 * Enables the wait status for the "Triebfahrzeug" with the specified
	 * address by sending the command <code>WAIT &lt;address&gt;</code> to the
	 * server.
	 * 
	 * @param address
	 *            The address of the Triebfahrzeug
	 * @return <code>true</code> if the wait was successful
	 * @throws IllegalArgumentException
	 *             thrown if the input parameters are invalid
	 * @throws FahrSteuerungException
	 * @see #parseResponse(String)
	 * @see #getLastResponse()
	 */
	@Override
	public boolean wait(String address) throws FahrSteuerungException {
		String command = null;
		String response = null;

		assertIsStarted();
		assertNotNullAndEmpty(address, "address");

		command = "WAIT " + address;
		response = sendAndReceive(command, telnetConnectionFst);

		try {
			return parseResponse(response);
		} catch (RuntimeException re) {
			throw new FahrSteuerungException("Parsing WAIT response failed", re);
		}
	}

	/**
	 * Disable the wait status for the "Triebfahrzeug" with the specified
	 * address by sending the command <code>ENDW &lt;address&gt;</code> to the
	 * server.
	 * 
	 * @param address
	 *            The address of the Triebfahrzeug
	 * @return <code>true</code> if the endw was successful
	 * @throws IllegalArgumentException
	 *             thrown if the input parameters are invalid
	 * @throws FahrSteuerungException
	 * @see #parseResponse(String)
	 * @see #getLastResponse()
	 */
	@Override
	public boolean endw(String address) throws FahrSteuerungException {
		String command = null;
		String response = null;

		assertIsStarted();
		assertNotNullAndEmpty(address, "address");

		command = "ENDW " + address;
		response = sendAndReceive(command, telnetConnectionFst);

		try {
			return parseResponse(response);
		} catch (RuntimeException re) {
			throw new FahrSteuerungException("Parsing ENDW response failed", re);
		}
	}

	/**
	 * Set or delete the number of the train for the "Triebfahrzeug" with the
	 * specified address by sending the command
	 * <code>ZN &lt;address&gt; &lt;zn&gt;</code> to the server.
	 * 
	 * @param address
	 *            The address of the Triebfahrzeug
	 * @param zn
	 *            The new train number, (if the number is <code>0</code>, the
	 *            number will be deleted)
	 * @return <code>true</code> if the zn was successful
	 * @throws IllegalArgumentException
	 *             thrown if the input parameters are invalid
	 * @throws FahrSteuerungException
	 * @see #parseResponse(String)
	 * @see #getLastResponse()
	 */
	@Override
	public boolean zn(String address, String zn) throws FahrSteuerungException {
		String command = null;
		String response = null;

		assertIsStarted();
		assertNotNullAndEmpty(address, "address");
		assertNotNullAndEmpty(zn, "zn");

		command = "ZN " + address + " " + zn;
		response = sendAndReceive(command, telnetConnectionFst);

		try {
			return parseResponse(response);
		} catch (RuntimeException re) {
			throw new FahrSteuerungException("Parsing ZN response failed", re);
		}
	}

	/**
	 * Return the number of the train for the "Triebfahrzeug" with the specified
	 * address.
	 * 
	 * @param address
	 *            The address of the Triebfahrzeug
	 * @return The train number, will be <code>null</code> if no number is
	 *         currently set
	 * @throws IllegalArgumentException
	 *             thrown if the input parameters are invalid
	 * @throws FahrSteuerungException
	 */
	@Override
	public String zn(String address) throws FahrSteuerungException {
		String command = null;
		String response = null;

		assertIsStarted();
		assertNotNullAndEmpty(address, "address");

		command = "ZN " + address;
		response = sendAndReceive(command, telnetConnectionFst);

		try {
			return parseZnResponse(response);
		} catch (RuntimeException re) {
			throw new FahrSteuerungException("Parsing ZN response failed", re);
		}
	}
	
	/**
	 * Add the "Triebfahrzeug" with the slave address to the "Triebfahrzeug"
	 * with the master address for multitraction by sending the command
	 * <code>MT &lt;master&gt; +&lt;slave&gt;</code> to the server.
	 * 
	 * @param master
	 *            The address of the master Triebfahrzeug
	 * @param slave
	 *            The address of the slave Triebfahrzeug to add to the master
	 * @return <code>true</code> if adding the slave to the master was
	 *         successful
	 * @throws IllegalArgumentException
	 *             thrown if the input parameters are invalid
	 * @throws FahrSteuerungException
	 * @see #parseResponse(String)
	 * @see #getLastResponse()
	 */
	@Override
	public boolean addMT(String master, String slave) throws FahrSteuerungException {
		String command = null;
		String response = null;

		assertIsStarted();
		assertNotNullAndEmpty(master, "master");
		assertNotNullAndEmpty(slave, "slave");

		command = "MT " + master + " +" + slave;
		response = sendAndReceive(command, telnetConnectionFst);

		try {
			return parseResponse(response);
		} catch (RuntimeException re) {
			throw new FahrSteuerungException("Parsing MT+ response failed", re);
		}
	}

	/**
	 * Remove the "Triebfahrzeug" with the slave address from the
	 * "Triebfahrzeug" with the master address for multitraction by sending the
	 * command <code>MT &lt;master&gt; -&lt;slave&gt;</code> to the server.
	 * 
	 * @param master
	 *            The address of the master Triebfahrzeug
	 * @param slave
	 *            The address of the slave Triebfahrzeug to remove from the
	 *            master
	 * @return <code>true</code> if removing the slave from the master was
	 *         successful
	 * @throws IllegalArgumentException
	 *             thrown if the input parameters are invalid
	 * @throws FahrSteuerungException
	 * @see #parseResponse(String)
	 * @see #getLastResponse()
	 */
	@Override
	public boolean removeMT(String master, String slave) throws FahrSteuerungException {
		String command = null;
		String response = null;

		assertIsStarted();
		assertNotNullAndEmpty(master, "master");
		assertNotNullAndEmpty(slave, "slave");

		command = "MT " + master + " -" + slave;
		response = sendAndReceive(command, telnetConnectionFst);

		try {
			return parseResponse(response);
		} catch (RuntimeException re) {
			throw new FahrSteuerungException("Parsing MT- response failed", re);
		}
	}

	/**
	 * Set the "Triebfahrzeug" with the passed address as ready for departure by
	 * sending the command <code>FERTIG &lt;address&gt;</code> to the server.
	 * 
	 * @param address
	 *            The address of the Triebfahrzeug
	 * @return <code>true</code> if fertig was successful
	 * @throws IllegalArgumentException
	 *             thrown if the input parameters are invalid
	 * @throws FahrSteuerungException
	 * @see #parseResponse(String)
	 * @see #getLastResponse()
	 */
	@Override
	public boolean fertig(String address) throws FahrSteuerungException {
		String command = null;
		String response = null;

		assertIsStarted();
		assertNotNullAndEmpty(address, "address");

		command = "FERTIG " + address;
		response = sendAndReceive(command, telnetConnectionFst);

		try {
			return parseResponse(response);
		} catch (RuntimeException re) {
			throw new FahrSteuerungException("Parsing FERTIG response failed", re);
		}
	}

	/**
	 * Set the "Triebfahrzeug" with the passed address to the passed
	 * "Handregler" by sending the command
	 * <code>HNDR &lt;reglerNr&gt; &lt;address&gt;</code> to the server.
	 * 
	 * @param address
	 *            The address of the Triebfahrzeug
	 * @param reglerNr
	 *            The number of the Handregler
	 * @return <code>true</code> if setting to the handregler was successful
	 * @throws IllegalArgumentException
	 *             thrown if the input parameters are invalid
	 * @throws FahrSteuerungException
	 */
	@Override
	public boolean hndr(String address, int reglerNr) throws FahrSteuerungException {

		String command = null;
		String response = null;

		assertIsStarted();
		assertNotNullAndEmpty(address, "address");
		assertInRange(reglerNr, "reglerNr", 0, 99);

		command = "HNDR " + Integer.toString(reglerNr) + " " + address;
		response = sendAndReceive(command, telnetConnectionFst);

		try {
			return parseResponse(response);
		} catch (RuntimeException re) {
			throw new FahrSteuerungException("Parsing HNDR response failed", re);
		}
	}

	/**
	 * Return the last response that has been received from the execution of the
	 * last command. May be used if one of the command functions returns
	 * <code>false</code> to be able to display the cause of the error (which is
	 * normally contained in the response of the command).<br>
	 * Each call to any of the command functions overwrites the previous
	 * response.
	 * 
	 * @return the last response (will be <code>null</code> if no command had
	 *         been executed before)
	 */
	@Override
	public String getLastResponse() {
		return lastResponse;
	}

	/**
	 * Start and execute the loop that sends the ping requests to the
	 * <code>FahrSteuerungControl</code> application.
	 */
	@Override
	public void run() {
		boolean connected = false;

		try {
			do {
				try {
					if (!connected) {
						telnetConnectionFstCtrl.connect();
						connected = true;
						pingLoopException = null;
					}
					sendToFahrSteuerungControl(FstCtrlCommand.PING, null);
				} catch (Exception e) {
					pingLoopException = e;
					connected = false;
					telnetConnectionFstCtrl.disconnect();
					Util.sleep(5000);
				}
			} while (!Util.waitForLatch(pingLatch, FSTCTRL_PING_INTERVAL));
		} finally {
			pingLoopException = null;
			if (connected) {
				sendAndReceive("END", telnetConnectionFstCtrl);
				telnetConnectionFstCtrl.disconnect();
			}
		}
	}

	/**
	 * Start the thread that sends the ping request to the
	 * <code>FahrSteuerungControl</code>.
	 */
	void startPingLoop() {
		if (controlled) {
			pingLatch = new CountDownLatch(1);
			threadPool = Executors.newSingleThreadExecutor();
			threadPool.execute(this);
		}
	}

	/**
	 * Stop the thread that sends the ping request to the
	 * <code>FahrSteuerungControl</code>.
	 */
	void stopPingLoop() {
		if (controlled) {
			pingLatch.countDown();
			threadPool.shutdown();
		}
	}

	/**
	 * Send the passed <code>command</code> to the telnet server and return its
	 * response.
	 * 
	 * @param command
	 *            The command to send
	 * @param client
	 *            The telnet connection client to use
	 * @return The telnet server's response
	 * @throws FahrSteuerungException
	 */
	String sendAndReceive(String command, TelnetConnectionClient client) throws FahrSteuerungException {
		try {
			client.write(command);
			return client.read();
		} catch (RuntimeException re) {
			throw new FahrSteuerungException("Communication failed", re);
		}
	}

	/**
	 * @param command
	 * @param address
	 * @throws FahrSteuerungException
	 */
	void sendToFahrSteuerungControl(FstCtrlCommand command, String address) throws FahrSteuerungException {

		if (controlled) {
			String request = FSTCTRL_PARAM_COMMAND + "=" + command.name();
			if (address != null) {
				request = request + "&" + FSTCTRL_PARAM_TFZ + "=" + address;
			}

			String response = sendAndReceive(request, telnetConnectionFstCtrl);

			if (!response.startsWith("OK")) {
				throw new FahrSteuerungException(response, pingLoopException);
			}
		}
	}

	/**
	 * Set the <code>started</code> flag.
	 * 
	 * @param isStarted
	 *            The new value
	 */
	void setStarted(boolean isStarted) {
		this.started = isStarted;
	}

	/**
	 * Assert that the current lifecycle status is <code>started</code> and if
	 * not, an <code>FahrSteuerungException</code> is thrown.
	 * 
	 * @throws FahrSteuerungException
	 *             Thrown if the current lifecycle is not <code>started</code>
	 */
	void assertIsStarted() {
		if (!isStarted()) {
			throw new FahrSteuerungException("FahrSteuerungsAdapter is not started, call start() first");
		}
	}

	/**
	 * Parse the response to check whether the command has been executed
	 * successfully or not.
	 * 
	 * @param response
	 *            The response of the executed command
	 * @return <code>true</code> if the response doesn't contain
	 *         <code>Fehler</code>
	 */
	boolean parseResponse(String response) {
		lastResponse = response;
		return !response.contains(RESPONSE_ERROR);
	}

	/**
	 * Parse the response of the <code>INF</code> command that returns
	 * information about registered "Triebfahrzeuge". Each registered
	 * "Triebfahrzeug" is represented by a <code>Tfz</code> instance.
	 * 
	 * @param response
	 *            The response of the INF command
	 * @return The list of registered "Triebfahrzeuge" (will be
	 *         <code>empty</code> and not <code>null</code> if none are
	 *         registered ), ordered by their address and mode
	 * @see #orderTfzBlocks(String[])
	 * @see TfzComparator
	 */
	List<Tfz> parseInfResponse(String response) {
		Map<String, Tfz> tfzReferenceMap = new HashMap<String, Tfz>();
		List<Tfz> tfzList = new ArrayList<Tfz>();
		Tfz tfz = null;

		lastResponse = response;
		String[] tfzInfoBlock = response.split("\\n- ");

		// The first block contains the command response status
		// this block is skipped
		if ((tfzInfoBlock != null) && (tfzInfoBlock.length > 1)) {

			for (String orderedTfzInfoBlock : orderTfzBlocks(tfzInfoBlock)) {
				tfz = new Tfz();

				// Get the common tfz information
				Matcher m = TFZ_INFO_PATTERN.matcher(orderedTfzInfoBlock);
				if (m.matches()) {
					tfz.setAddress(m.group(1));
					tfz.setSpeed(Integer.parseInt(m.group(2)));

					if (!LOCATION_NONE.equals(m.group(3))) {
						tfz.setLocation(m.group(3));
					} else {
						tfz.setLocation("-");
					}

					// Groups 4-8 are the special function indicators, e.g.
					// F0---4
					for (int groupIndex = 4; groupIndex <= 8; groupIndex++) {
						tfz.getFunction()[groupIndex - 4] = ("-".equals(m.group(groupIndex))) ? false : true;
					}

					tfzReferenceMap.put(tfz.getAddress(), tfz);

					// Get the master tfz if in multitraction mode
					if (orderedTfzInfoBlock.contains(TFZ_MULTITRAKTION)) {
						tfz.setMode(Mode.MULTITRACTION);
						tfz.setSlaveTfz(true);

						m = TFZ_MT_PATTERN.matcher(orderedTfzInfoBlock);
						if (m.matches()) {
							Tfz master = tfzReferenceMap.get(m.group(1));
							if (master != null) {
								tfz.setMaster(master);
								master.getSlaves().add(tfz);
							}
						}
					} else {
						if (orderedTfzInfoBlock.contains(TFZ_AUTOMATIC)) {
							tfz.setMode(Mode.AUTO);
						} else {
							tfz.setMode(Mode.MANUAL);
						}
					}

					// Check handregler
					m = TFZ_HNDR_PATTERN.matcher(orderedTfzInfoBlock);
					if (m.matches()) {
						tfz.setHandregler(Integer.parseInt(m.group(1)));
					}

					// Check direction
					if (orderedTfzInfoBlock.contains(TFZ_PFEILR)) {
						tfz.setDirection(Direction.ARROW);
					} else {
						tfz.setDirection(Direction.REVERSE);
					}

					// check for reversed status
					if (orderedTfzInfoBlock.contains(TFZ_UMK)) {
						tfz.setReverse(true);
					}

					// check for hold status
					if (orderedTfzInfoBlock.contains(TFZ_HOLD)) {
						tfz.setHold(true);
					}

					// check for wait status
					if (orderedTfzInfoBlock.contains(TFZ_WAIT)) {
						tfz.setWait(true);
					}

					// check for Befehl 2 status
					if (orderedTfzInfoBlock.contains(TFZ_BEFEHL2)) {
						tfz.setBefehl2(true);
					}

					// check for ready status
					if (orderedTfzInfoBlock.contains(TFZ_FAHRB)) {
						tfz.setReady(true);
					}
					
					// check for multitraktion master status
					if (orderedTfzInfoBlock.contains(TFZ_MT)) {
						tfz.setMasterTfz(true);
					}

					tfzList.add(tfz);
				}
			}
		}

		Collections.sort(tfzList, tfzComparator);
		return tfzList;
	}

	/**
	 * Parse the response of the <code>ZN</code> command that returns
	 * the train number for a "Triebfahrzeuge".
	 * 
	 * @param response
	 *            The response of the ZN command
	 * @return The train number, will be <code>null</code> if no number is
	 *         currently set
	 */
	String parseZnResponse(String response) {
		lastResponse = response;

		Matcher m = TFZ_ZN_PATTERN.matcher(response);
		if (m.matches()) {
			return m.group(1);
		}

		return null;
	}
	
	/**
	 * Order the items in the passed array so that the "Triebfahrzeuge" in
	 * multitraction mode are at the end of the list. This is required to be
	 * able to parse all master "Triebfahrzeuge" before the multitraction
	 * "Triebfahrzeuge" to be sure to find the master "Triebfahrzeug" to set it
	 * as the master reference for a multitraction "Triebfahrzeug". Also
	 * replaces the new-line characters in the response of each item with
	 * spaces.
	 * 
	 * @param tfzInfoBlock
	 *            The unsorted array of registered "Triebfahrzeuge"
	 * @return The sorted list of registered "Triebfahrzeuge"
	 * @see MultitraktionComparator
	 */
	List<String> orderTfzBlocks(String[] tfzInfoBlock) {
		List<String> orderedList = new ArrayList<String>();

		for (int index = 1; index < tfzInfoBlock.length; index++) {
			orderedList.add(tfzInfoBlock[index].replace('\r', ' ').replace('\n', ' '));
		}

		Collections.sort(orderedList, multitraktionComparator);
		return orderedList;
	}

	/**
	 * Comparator to sort the available tfz first by their mode (automatic and
	 * manual before multitraction) and second by their address in ascending
	 * order.
	 */
	public static class TfzComparator implements Comparator<Tfz> {
		public int compare(Tfz lhs, Tfz rhs) {
			int lhsAddress = Integer.parseInt(lhs.getAddress());
			int rhsAddress = Integer.parseInt(rhs.getAddress());

			if (lhs.getMode() != Mode.MULTITRACTION && rhs.getMode() != Mode.MULTITRACTION) {
				return (lhsAddress < rhsAddress) ? -1 : 1;
			} else if (lhs.getMode() == Mode.MULTITRACTION && rhs.getMode() == Mode.MULTITRACTION) {
				return (lhsAddress < rhsAddress) ? -1 : 1;
			} else if (lhs.getMode() == Mode.MULTITRACTION && rhs.getMode() != Mode.MULTITRACTION) {
				return 1;
			} else if (lhs.getMode() != Mode.MULTITRACTION && rhs.getMode() == Mode.MULTITRACTION) {
				return -1;
			} else {
				return 0;
			}
		}
	}

	/**
	 * Comparator to sort all multitraction tfz at the end of the set of
	 * available tfz.
	 */
	public static class MultitraktionComparator implements Comparator<String> {
		public int compare(String lhs, String rhs) {
			if ((lhs.contains(TFZ_MULTITRAKTION)) && (rhs.contains(TFZ_MULTITRAKTION))) {
				return 1;
			} else if ((lhs.contains(TFZ_MULTITRAKTION)) && (rhs.contains(TFZ_MULTITRAKTION))) {
				return -1;
			} else if ((lhs.contains(TFZ_MULTITRAKTION)) && (rhs.contains(TFZ_MULTITRAKTION))) {
				return -1;
			} else if ((lhs.contains(TFZ_MULTITRAKTION)) && (rhs.contains(TFZ_MULTITRAKTION))) {
				return 1;
			} else {
				return 0;
			}
		}
	}
}
